
package jason.asSyntax.directives;

import jason.JasonException;
import jason.asSemantics.Agent;
import jason.asSemantics.Agent.Module;
import jason.asSyntax.*;
import jason.asSyntax.parser.ParseException;
import jason.asSyntax.parser.Token;
import jason.asSyntax.parser.as2j;
import jason.mas2j.MAS2JProject;
import static jason.util.Util.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/** Implementation of the <code>import</code> directive. */
public class Import implements Directive {
	private static Logger logger = Logger.getLogger(Import.class.getName());
	public static final String CRPrefix = "ClassResource:";
	
	private static List<String> aslSourcePaths = null;
	
	public Agent process(Atom directive, Agent outerContent, Agent innerContent, Token token) {
		if (outerContent == null || !outerContent.isDoImports()) {
			logger.fine("\tDirective is ignored: " + directive);
			return null;
		}
		
		if (!(directive instanceof Importive)) {
			logger.log(Level.SEVERE, "The directive supposed to be an import, instead of: " + directive.getClass() + " of the directive: " + directive);
			return null;
		}
		
		Importive importive = (Importive)directive;
		String name = importive.getName();
		String path = importive.getPath();
		String reference = '[' + importive.getErrorMsg() + "] ";
		
		importModule(outerContent, name, path, reference, importive.getToken());
		
		return null;
	}
	
	public static void importModule(Agent outerContent, String name, String path, String reference) {
		importModule(outerContent, name, path, reference, null);
	}
	
	public static void importModule(Agent outerContent, String name, String path, String reference, Token token) {
		String agentDir;
		try {
			String aslSrc = outerContent.getASLSrc();
			String query = (new URL(aslSrc)).getQuery();
			agentDir = new URL((query == null ? aslSrc : aslSrc.substring(0, aslSrc.length() - query.length() - 1)) + "/..").toURI().normalize().toString();
		} catch (Exception e) {
			agentDir = new File(outerContent.getASLSrc()).getParentFile().getAbsolutePath();
		}
		boolean relativePath = false;
		if (path == null) {
			path = agentDir + File.separator + name;
			relativePath = true;
		} else {
			boolean isUrl;
			try {
				new URL(path);
				isUrl = true;
			} catch (MalformedURLException e) {
				isUrl = false;
			}
			
			if (!(new File(path)).isAbsolute() && !isUrl) {
				path = agentDir + File.separator + path;
				relativePath = true;
			}
		}
		
		if (relativePath) {
			if (!path.endsWith('.' + MAS2JProject.ASM_EXT) && new File(path + '.' + MAS2JProject.ASM_EXT).exists()) {
				path += '.' + MAS2JProject.ASM_EXT;
			} else if (!path.endsWith('.' + MAS2JProject.AS_EXT) && new File(path + '.' + MAS2JProject.AS_EXT).exists()) {
				path += '.' + MAS2JProject.AS_EXT;
			}
		}
		
		Pair<String, InputStream> absPathAndStream;
		try {
			absPathAndStream = getAbsPathAndStream(outerContent.getASLSrc(), path, true);
		} catch (FileNotFoundException e) {
			throw new RuntimeException("", new ParseException(reference + "The imported module's file '" + path + "' was not found!\n\t(It is being imported in the agent '" + outerContent.getASLSrc() + "' with module name '" + name + "'.)", token));
		} catch (Exception e) {
			throw new RuntimeException("", new ParseException(reference + "The imported module's file '" + path + "' could not be imported!\n\t" + e.toString() + "\n\t(It is being imported in the agent '" + outerContent.getASLSrc() + "' with module name '" + name + "'.)", token));
		}
		
		path = absPathAndStream.fst;
		InputStream in = absPathAndStream.snd;
		
		boolean isBaseImported = false;
		try {
			PriorityQueue<Importer> importQueue = outerContent.getImportQueue();
			if (importQueue == null) {
				outerContent.setImportQueue(importQueue = new PriorityQueue<Importer>());
			}
			
			String importedUnderModuleName = outerContent.getImportedUnderModuleName();
			if (importedUnderModuleName == null) {
				importedUnderModuleName = name;
			} else {
				importedUnderModuleName += Agent.Module.OPERATOR + name;
			}
			
			Module module = outerContent.getModuleImported(path);
			if (module == null) {
				TreeSet<String> importChain = outerContent.getImportChain();
				if (importChain.contains(path)) {
					Agent ag = new Agent();
					ag.initAg();
					ag.setLevel(outerContent.getLevel() + 1); // to make sure we don't mess with importQueue
					ag.setDoImport(false); // --> no modularity fields required to be set
					
					ag.setASLSrc(path);
					as2j parser = new as2j(in);
					parser.agent(ag);
					outerContent.addModule(name, importedUnderModuleName, path, ag, reference, token);
					
					logger.fine("as2j: AgentSpeak module '" + path + "' successfully parsed for exports!");
				} else {
					Agent ag = new Agent();
					ag.initAg();
					ag.setASLSrc(path);
					ag.setSideKick(outerContent.isSideKick());
					ag.setLevel(outerContent.getLevel() + 1);
					ag.setImportQueue(importQueue);
					ag.getImportChain().addAll(importChain);
					ag.getImportChain().add(path);
					ag.propagateForImported(outerContent);
					ag.setImportedUnderModuleName(importedUnderModuleName);
					as2j parser = new as2j(in);
					
					parser.agent(ag);
					
					Module newModule = outerContent.addModule(name, importedUnderModuleName, path, ag, reference, token);
					
					importQueue.add(new Importer(ag, newModule));
					logger.fine("as2j: AgentSpeak module '" + path + "' parsed successfully!");
				}
			} else {
				if (module.getExports() == null) isBaseImported = true;
				
				Module newModule = outerContent.addModule(name, importedUnderModuleName, module, reference, token);
				if (isBaseImported) { // base agent --> its exports are parsed now
					outerContent.replaceModule(path, newModule);
					logger.fine("as2j: AgentSpeak module '" + path + "' successfully parsed for exports!");
				}
				importQueue.add(new Importer(newModule.getAgent(), newModule));
			}
		} catch (Throwable e) {
			if (outerContent.isSideKick()) {
				if (isBaseImported) return;
				
				if (e instanceof ParseException) {
					ParseException pe = (ParseException)e;
					if(!pe.isIgnoreSubsequentMessages()) pe.currentToken = token;
				}
			}
			
			throw new RuntimeException("in module \"" + path + "\": ", e);
		}
	}
	
	public static void importModules(Agent agent, boolean dynamically) throws JasonException, ParseException {
		if (agent == null || agent.getLevel() != 0 || !agent.isDoImports()) return; // only import after the base agent has been completely processed
		PriorityQueue<Importer> importQueue = agent.getImportQueue();
		if (importQueue == null) return;
		
		while (!importQueue.isEmpty()) {
			Importer imp = importQueue.poll();
			if (imp.agent != null && agent.moduleApplied(imp.module.getPath())) {
				imp.agent.applyModulePrefix(agent, imp.module.getName(), imp.module.getPrefix());
				agent.importComponents(imp.agent, dynamically);
			}
		}
	}
	
	public static void setSourcePaths(List<String> sp) {
		aslSourcePaths = sp;
	}
	
	public static Pair<String, InputStream> getAbsPathAndStream(String outerPrefix, String path, boolean needStream) throws Exception {
		InputStream in = null;
		
		// outerPrefix = the source file that has the include directive
		if (/*outerContent != null && */outerPrefix != null) {
			// check if the outer is URL
			if (outerPrefix.startsWith("jar:")) {
				outerPrefix = outerPrefix.substring(0, outerPrefix.indexOf("!") + 1) + "/";
				path = checkPathAndFixWithSourcePath(path, aslSourcePaths, outerPrefix);
				try {
					URL url = new URL(path);
					try {
						path = url.toURI().normalize().toString();
					} catch (Exception ex) {
					}
					if (needStream) in = url.openStream();
				} catch (Exception e) {
					URL url = Agent.class.getResource(path);
					if (url == null) throw e;
					try {
						path = url.toURI().normalize().toString();
					} catch (Exception ex) {
					}
					if (needStream) in = url.openStream();
				}
			} else if (outerPrefix.startsWith(CRPrefix)) {
				// outer is loaded from a resource ("application".jar) file, used for java web start
				int posSlash = outerPrefix.lastIndexOf("/");
				
				List<String> newpath = new ArrayList<String>();
				if (outerPrefix.indexOf("/") != posSlash) { // has only one slash
					newpath.add(outerPrefix.substring(CRPrefix.length() + 1, posSlash));
				}
				newpath.addAll(aslSourcePaths);
				
				path = checkPathAndFixWithSourcePath(path, newpath, CRPrefix + "/");
				try {
					URL url = new URL(path);
					try {
						path = url.toURI().normalize().toString();
					} catch (Exception ex) {
					}
					if (needStream) in = url.openStream();
				} catch (Exception e) {
					URL url = Agent.class.getResource(path.substring(CRPrefix.length()));
					if (url == null) throw new FileNotFoundException();
					try {
						path = url.toURI().normalize().toString();
					} catch (Exception ex) {
					}
					if (needStream) in = url.openStream();
				}
			} else {
				// get the directory of the source of the outer agent and 
				// try to find the included source in the same directory
				// or in the source paths
				List<String> newpath = new ArrayList<String>();
				newpath.add(new File(outerPrefix).getAbsoluteFile().getParent());
				if (aslSourcePaths != null) newpath.addAll(aslSourcePaths);
				
				try {
					URL u = new URL(path);
					path = u.getFile();
					String urlstr = u.toString();
					String protocol = null;
					
					if (path.isEmpty()) {
						protocol = urlstr;
					} else {
						int pos = urlstr.indexOf(path);
						if (pos >= 0) {
							protocol = urlstr.substring(0, pos);
						}
					}
					
					String prevPath = path;
					path = checkPathAndFixWithSourcePath(path, newpath, protocol);
					if (prevPath.equals(path)) path = protocol + path;
					
					if (needStream) try {
						URL url = new URL(path);
						try {
							path = url.toURI().normalize().toString();
						} catch (Exception ex) {
						}
						in = url.openStream();
						
					} catch (Exception e) {
						URL url = Agent.class.getResource(path);
						if (url == null) throw e;
						try {
							path = url.toURI().normalize().toString();
						} catch (Exception ex) {
						}
						in = url.openStream();
					}
				} catch (MalformedURLException e) {
					path = checkPathAndFixWithSourcePath(path, newpath, null);
					if (needStream) in = new FileInputStream(path);
				}
			}
		} else {
			path = checkPathAndFixWithSourcePath(path, aslSourcePaths, null);
			if (needStream) in = new FileInputStream(path);
		}
		path = getAbsPath(path);
		return new Pair<String, InputStream>(path, in);
	}
	
	public static File checkPathAndGetFile(String path, List<String> srcpath, String outerPrefix) {
		if (outerPrefix != null) {
			// check if the outer is URL
			if (outerPrefix.startsWith("jar:")) {
				outerPrefix = outerPrefix.substring(0, outerPrefix.indexOf("!") + 1) + "/";
				path = checkPathAndFixWithSourcePath(path, aslSourcePaths, outerPrefix);
				try {
					URL url = new URL(path);
					try {
						return new File(url.toURI().normalize());
					} catch (Exception ex) {
					}
					try {
						return new File(url.toURI());
					} catch (Exception ex2) {
						return new File(url.toString());
					}
				} catch (Exception e) {
					URL url = Agent.class.getResource(path);
					if (url == null) return null;
					try {
						return new File(url.toURI().normalize());
					} catch (Exception ex) {
					}
					try {
						return new File(url.toURI());
					} catch (Exception ex2) {
						return new File(url.toString());
					}
				}
			} else if (outerPrefix.startsWith(CRPrefix)) {
				// outer is loaded from a resource ("application".jar) file, used for java web start
				int posSlash = outerPrefix.lastIndexOf("/");
				
				List<String> newpath = new ArrayList<String>();
				if (outerPrefix.indexOf("/") != posSlash) { // has only one slash
					newpath.add(outerPrefix.substring(CRPrefix.length() + 1, posSlash));
				}
				newpath.addAll(aslSourcePaths);
				
				path = checkPathAndFixWithSourcePath(path, newpath, CRPrefix + "/");
				
				try {
					URL url = new URL(path);
					try {
						return new File(url.toURI().normalize());
					} catch (Exception ex) {
					}
					try {
						return new File(url.toURI());
					} catch (Exception ex2) {
						return new File(url.toString());
					}
				} catch (Exception e) {
					URL url = Agent.class.getResource(path.substring(CRPrefix.length()));
					if (url == null) return null;
					try {
						return new File(url.toURI().normalize());
					} catch (Exception ex) {
					}
					try {
						return new File(url.toURI());
					} catch (Exception ex2) {
						return new File(url.toString());
					}
				}
			} else {
				// get the directory of the source of the outer agent and 
				// try to find the included source in the same directory
				// or in the source paths
				List<String> newpath = new ArrayList<String>();
				newpath.add(new File(outerPrefix).getAbsoluteFile().getParent());
				if (aslSourcePaths != null) newpath.addAll(aslSourcePaths);
				
				try {
					URL u = new URL(path);
					path = u.getFile();
					String urlstr = u.toString();
					String protocol = null;
					
					if (path.isEmpty()) {
						protocol = urlstr;
					} else {
						int pos = urlstr.indexOf(path);
						if (pos >= 0) {
							protocol = urlstr.substring(0, pos);
						}
					}
					
					String prevPath = path;
					path = checkPathAndFixWithSourcePath(path, newpath, protocol);
					if (prevPath.equals(path)) path = protocol + path;
					
					try {
						URL url = new URL(path);
						try {
							return new File(url.toURI().normalize());
						} catch (Exception ex) {
						}
						try {
							return new File(url.toURI());
						} catch (Exception ex2) {
							return new File(url.toString());
						}
					} catch (Exception e) {
						URL url = Agent.class.getResource(path);
						if (url == null) return null;
						try {
							return new File(url.toURI().normalize());
						} catch (Exception ex) {
						}
						try {
							return new File(url.toURI());
						} catch (Exception ex2) {
							return new File(url.toString());
						}
					}
				} catch (MalformedURLException e) {
					path = checkPathAndFixWithSourcePath(path, newpath, null);
					return new File(path);
				}
			}
		} else {
			path = checkPathAndFixWithSourcePath(path, aslSourcePaths, null);
			return new File(path);
		}
	}
	
	/** fix path of the asl code based on aslSourcePath, also considers code from a jar file (if urlPrefix is not null) */
	private static String checkPathAndFixWithSourcePath(String f, List<String> srcpath, String urlPrefix) {
		if (urlPrefix == null || urlPrefix.length() == 0) {
			if (new File(f).exists()) {
				return f;
			} else if (srcpath != null) {
				for(String path : srcpath) {
					try {
						File newname = new File(path + "/" + f.toString());
						if (newname.exists()) {
							return newname.getCanonicalFile().toString();
						}
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		} else {
			if (testURLSrc(urlPrefix + f)) {
				return urlPrefix + f;
			} else if (srcpath != null) {
				for(String path : srcpath) {
					String newname = urlPrefix + path + "/" + f;
					if (testURLSrc(newname)) {
						return newname;
					}
				}
			}
		}
		return f;
	}
	
	private static boolean testURLSrc(String asSrc) {
		try {
			if (asSrc.startsWith(CRPrefix)) {
				Agent.class.getResource(asSrc.substring(CRPrefix.length())).openStream();
				return true;
			} else {
				try {
					new URL(asSrc).openStream();
					return true;
				} catch (Exception e2) {
					Agent.class.getResource(asSrc).openStream();
					return true;
				}
			}
		} catch (Exception e) {
		}
		return false;
	}
	
	public static class Importer implements Comparable<Importer> {
		final int level;
		final Agent agent;
		final Module module;
		
		public Importer(Agent agent, Module module) {
			this.agent = agent;
			this.module = module;
			this.level = (agent == null ? 0 : agent.getLevel());
		}
		
		public int compareTo(Importer o) {
			return level - o.level;
		}
		
		public String toString() {
			return module.toString();
		}
	}
}
